<!doctype html>
<html lang="zh-CN">
<head>

    <meta charset="utf-8">
    <meta name="generator" content="Hugo 0.74.1" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <title>Android Q 隐私篇(1) - 外部储存 | caijinglong的博客</title>
    <meta property="og:title" content="Android Q 隐私篇(1) - 外部储存 - caijinglong的博客">
    <meta property="og:type" content="article">
        
    <meta property="article:published_time" content="2019-05-20T09:43:48&#43;08:00">
        
        
    <meta property="article:modified_time" content="2019-05-20T09:43:48&#43;08:00">
        
    <meta name="Keywords" content="golang,go语言,flutter,caijinglong,java,android,博客,项目管理,python,软件架构,公众号,小程序">
    <meta name="description" content="Android Q 隐私篇(1) - 外部储存">
        
    <meta name="author" content="caijinglong">
    <meta property="og:url" content="http://www.kikt.top/posts/android/android-q/privacy/android-q-privacy-1/">
    <link rel="shortcut icon" href="/favicon.ico" type="image/x-icon">

    <link rel="stylesheet" href="/css/normalize.css">
    
        <link rel="stylesheet" href="/css/prism.css">
    
    <link rel="stylesheet" href="/css/style.css">
    <script type="text/javascript" src="//cdn.bootcss.com/jquery/3.2.1/jquery.min.js"></script>

    

   
    <script>
        (function(){
            var bp = document.createElement('script');
            var curProtocol = window.location.protocol.split(':')[0];
            if (curProtocol === 'https') {
                bp.src = 'https://zz.bdstatic.com/linksubmit/push.js';
            }
            else {
                bp.src = 'http://push.zhanzhang.baidu.com/push.js';
            }
            var s = document.getElementsByTagName("script")[0];
            s.parentNode.insertBefore(bp, s);
        })();
    </script>

    
    
</head>

<body>
<header id="header" class="clearfix">
    <div class="container">
        <div class="col-group">
            <div class="site-name ">
                
                    <a id="logo" href="http://www.kikt.top">
                        caijinglong的博客
                    </a>
                
                <p class="description">Android/Flutter开发者，对于golang/python/ios/java 均有所了解</p>
            </div>
            <div>
                <nav id="nav-menu" class="clearfix">
                    <a class="" href="http://www.kikt.top">首页</a>
                    
                    <a  href="http://www.kikt.top/archives/" title="归档">归档</a>
                    
                    <a  href="http://www.kikt.top/contact/" title="接单">接单</a>
                    
                    <a  href="http://www.kikt.top/about/" title="关于">关于</a>
                    
                </nav>
            </div>
        </div>
    </div>
</header>


<div id="body">
    <div class="container">
        <div class="col-group">

            <div class="col-8" id="main">
                <div class="res-cons">
                    <article class="post">
                        <header>
                            <h1 class="post-title">Android Q 隐私篇(1) - 外部储存</h1>
                        </header>
                        <date class="post-meta meta-date">
                            2019年5月20日
                        </date>
                        
                        <div class="post-meta meta-category">
                            |
                            
                                <a href="http://www.kikt.top/categories/android">android</a>
                            
                        </div>
                        
                        
                        <div class="post-meta">
                            <span id="busuanzi_container_page_pv">|<span id="busuanzi_value_page_pv"></span><span> 阅读</span></span>
                        </div>
                        
                        <div class="post-content">
                            <p>一年一度的 googleIO 开完以后,不出意外的 Android10.0 系统(AndroidQ)出来了</p>
<p>隐私配置又㕛叒叕更新了..</p>
<p>连接地址: <a href="https://developer.android.com/preview/privacy">https://developer.android.com/preview/privacy</a>
这个地址可能在来年就变成 android 11 的了, 所以仅保证在<strong>2019 年 05 月 20 日以及之后的一段时间内</strong>有效</p>
<p>主要包含以下五大项目</p>
<p><img src="https://dev.azure.com/cjlspy/844861b8-ee45-48a3-b430-b974684baaaa/_apis/git/repositories/bf4da1c1-1cfa-4f54-8a75-745cce2b737c/items?versionDescriptor%5Bversion%5D=master&amp;path=%2F20190520100845.png&amp;versionDescriptor%5BversionOptions%5D=0&amp;versionDescriptor%5BversionType%5D=0&amp;resolveLfs=true&amp;%24format=octetStream&amp;api-version=5.0" alt="20190520100845.png"></p>
<p>对于大部分应用来说,储存方式的更新会有所影响, 其他的可能都是 SDK 要做的事情,和普通开发者关系不大</p>
<p>在适配新的系统前就和从前一样,暂时不升级 targetVersion,把 targetVersion 设置为 28 以下就不会影响旧程序了,就如同以前运行时权限真的是坑到爆炸,但是暂时不适配也是可以的</p>
<p>但是未来无论如何都需要适配新系统,所以先来看看</p>
<p>本篇只说储存的方式和权限问题,其他暂时略过不表</p>
<h2 id="目录">目录</h2>
<ul>
<li><a href="#%E7%9B%AE%E5%BD%95">目录</a></li>
<li><a href="#%E6%A3%80%E6%9F%A5%E8%BF%81%E7%A7%BB%E6%83%85%E5%86%B5">检查迁移情况</a></li>
<li><a href="#%E5%BC%80%E5%8F%91%E7%8E%AF%E5%A2%83">开发环境</a>
<ul>
<li><a href="#%E6%9B%B4%E6%96%B0-sdk">更新 sdk</a></li>
<li><a href="#androidq-buildgradle">androidQ build.gradle</a></li>
</ul>
</li>
<li><a href="#%E5%A4%96%E9%83%A8%E5%82%A8%E5%AD%98">外部储存</a>
<ul>
<li><a href="#androidq-%E4%B8%8B%E8%AF%BB%E5%86%99%E6%96%87%E4%BB%B6">androidQ 下读写文件</a></li>
<li><a href="#%E5%A4%96%E9%83%A8%E5%82%A8%E5%AD%98%E7%9A%84%22%E6%B2%99%E7%AE%B1%22">外部储存的&quot;沙箱&rdquo;</a></li>
<li><a href="#%E7%9B%B8%E5%86%8C%E5%9B%BE%E7%89%87">相册图片</a>
<ul>
<li><a href="#%E9%9C%80%E8%A6%81%E7%94%A8-jni-%E5%A4%84%E7%90%86%E5%9B%BE%E7%89%87">需要用 jni 处理图片</a></li>
<li><a href="#%E4%BF%AE%E6%94%B9%E5%85%B6%E4%BB%96%E9%A1%B9%E7%9B%AE%E5%88%9B%E5%BB%BA%E7%9A%84%E6%96%87%E4%BB%B6">修改其他项目创建的文件</a></li>
<li><a href="#%E7%85%A7%E7%89%87%E7%9A%84%E4%BD%8D%E7%BD%AE%E4%BF%A1%E6%81%AF">照片的位置信息</a></li>
</ul>
</li>
</ul>
</li>
<li><a href="#sdcard-%E7%9B%AE%E5%BD%95%E7%BB%93%E6%9E%84">sdcard 目录结构</a></li>
<li><a href="#%E5%90%8E%E8%AE%B0">后记</a></li>
</ul>
<h2 id="检查迁移情况">检查迁移情况</h2>
<p><a href="https://developer.android.com/preview/privacy/checklist">https://developer.android.com/preview/privacy/checklist</a> 有一个表格提供了如何检查和迁移的方案</p>
<p><img src="https://dev.azure.com/cjlspy/844861b8-ee45-48a3-b430-b974684baaaa/_apis/git/repositories/bf4da1c1-1cfa-4f54-8a75-745cce2b737c/items?versionDescriptor%5Bversion%5D=master&amp;path=%2F20190520101927.png&amp;versionDescriptor%5BversionOptions%5D=0&amp;versionDescriptor%5BversionType%5D=0&amp;resolveLfs=true&amp;%24format=octetStream&amp;api-version=5.0" alt="20190520101927.png"></p>
<h2 id="开发环境">开发环境</h2>
<p>说一下开发环境</p>
<p>MacOS<br>
Android Studio 3.4.0<br>
android 9.0 设备一台<br>
androidQ 虚拟机一台(官方的 Emulator)</p>
<h3 id="更新-sdk">更新 sdk</h3>
<p>更新 sdk, 下载 androidQ 相关的 sdk/sdktools/模拟器</p>
<h3 id="androidq-buildgradle">androidQ build.gradle</h3>
<p>当前想尝试 androidQ 的话, 编译版本和目标版本号需要按如下方式设置, 今后的话可能是 29/30 之类的数字</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-groovy" data-lang="groovy">targetSdkVersion <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;Q&#39;</span>
compileSdkVersion <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;android-Q&#39;</span>
</code></pre></div><h2 id="外部储存">外部储存</h2>
<h3 id="androidq-下读写文件">androidQ 下读写文件</h3>
<p>权限和以前一样</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-xml" data-lang="xml">    <span style="color:#f92672">&lt;uses-permission</span> <span style="color:#a6e22e">android:name=</span><span style="color:#e6db74">&#34;android.permission.READ_EXTERNAL_STORAGE&#34;</span><span style="color:#f92672">/&gt;</span>
    <span style="color:#f92672">&lt;uses-permission</span> <span style="color:#a6e22e">android:name=</span><span style="color:#e6db74">&#34;android.permission.WRITE_EXTERNAL_STORAGE&#34;</span><span style="color:#f92672">/&gt;</span>
</code></pre></div><p>Activity 文件</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-kotlin" data-lang="kotlin"><span style="color:#66d9ef">package</span> top.kikt.camerax.scopedstorage

<span style="color:#66d9ef">import</span> android.os.Bundle
<span style="color:#66d9ef">import</span> android.util.Log
<span style="color:#66d9ef">import</span> androidx.appcompat.app.AppCompatActivity
<span style="color:#66d9ef">import</span> java.io.File

<span style="color:#66d9ef">class</span> <span style="color:#a6e22e">MainActivity</span> : AppCompatActivity() {

    <span style="color:#66d9ef">companion</span> <span style="color:#66d9ef">object</span> {
        <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">const</span> <span style="color:#66d9ef">val</span> TAG = <span style="color:#e6db74">&#34;MainActivity&#34;</span>
    }

    <span style="color:#66d9ef">override</span> <span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">onCreate</span>(savedInstanceState: Bundle?) {
        <span style="color:#66d9ef">super</span>.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        <span style="color:#66d9ef">val</span> path = externalCacheDir<span style="color:#f92672">?.</span>absoluteFile<span style="color:#f92672">?.</span>path

        Log.d(TAG, <span style="color:#e6db74">&#34;cache dir = $path&#34;</span>)

        <span style="color:#66d9ef">val</span> file = File(path, <span style="color:#e6db74">&#34;abc.txt&#34;</span>)

        file.writeText(<span style="color:#e6db74">&#34;我要往里写数据&#34;</span>)

        <span style="color:#66d9ef">val</span> text = file.readText()

        Log.d(TAG, <span style="color:#e6db74">&#34;数据是: $text&#34;</span>)
    }
}

</code></pre></div><pre><code class="language-log" data-lang="log">2019-05-20 11:41:28.938 7435-7435/top.kikt.camerax.scopedstorage D/MainActivity: cache dir = /storage/emulated/0/Android/data/top.kikt.camerax.scopedstorage/cache
2019-05-20 11:41:28.957 7435-7435/top.kikt.camerax.scopedstorage D/MainActivity: 数据是: 我要往里写数据
</code></pre><p>这里可以看出, 在正常情况下, 将文件存入外部储存比从前方便了, 不再需要动态权限申请</p>
<p>并且,我这里的小米手机<strong>没有</strong>弹出敏感权限的那个对话框</p>
<h3 id="外部储存的沙箱">外部储存的&quot;沙箱&rdquo;</h3>
<p>从前只要用户允许 app 访问外置储存后, 就可以通过 MediaStore 的 api 拿到图片和相册的完整数据</p>
<p>而 androidQ 中对于这部分权限进行了重新处理</p>
<p>按照文档的说法, 目前外部储存中是沙箱的模式, 除非你的目标文件属于以下三类, 否则其他应用将无法看到你的图片</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-md" data-lang="md">Photos, which are stored in MediaStore.Images.
Videos, which are stored in MediaStore.Video.
Music files, which are stored in MediaStore.Audio.
</code></pre></div><p><img src="https://dev.azure.com/cjlspy/844861b8-ee45-48a3-b430-b974684baaaa/_apis/git/repositories/bf4da1c1-1cfa-4f54-8a75-745cce2b737c/items?versionDescriptor%5Bversion%5D=master&amp;path=%2F20190520130327.png&amp;versionDescriptor%5BversionOptions%5D=0&amp;versionDescriptor%5BversionType%5D=0&amp;resolveLfs=true&amp;%24format=octetStream&amp;api-version=5.0" alt="20190520130327.png"></p>
<p>想要访问非自己 app 的特定的文件夹, 比如 downloads, 你需要使用<a href="https://developer.android.com/guide/topics/providers/document-provider"><code>Storage Access Framework</code></a>, 这是一个 android 4.4 加入的 api, 官方有中文说明, 网上应该也有很多示例代码, 这里不展开了</p>
<p>有一点需要注意: 当 app 被卸载后, <strong>位于外部储存中的 app 数据会被清除</strong>, 如果你需要保留数据, 必须保存到 MediaStore 中</p>
<p>因为是按顺序浏览文档说明, 我看到了如下的说明:</p>
<p><img src="https://dev.azure.com/cjlspy/844861b8-ee45-48a3-b430-b974684baaaa/_apis/git/repositories/bf4da1c1-1cfa-4f54-8a75-745cce2b737c/items?versionDescriptor%5Bversion%5D=master&amp;path=%2F20190520131322.png&amp;versionDescriptor%5BversionOptions%5D=0&amp;versionDescriptor%5BversionType%5D=0&amp;resolveLfs=true&amp;%24format=octetStream&amp;api-version=5.0" alt="20190520131322.png"></p>
<p>访问自己 app 位于外部储存中的内容不需要访问权限, 所以这里我注释掉清单文件中的权限, 发现表现和之前一样, 可以写入和读取文件, 这也就解释了为什么不会弹出敏感权限申请, 因为在 androidQ 中, 访问自己 app 的文件不再是敏感权限</p>
<h3 id="相册图片">相册图片</h3>
<p>完整的代码如下, 使用了 RxJava+RxPermission 做权限的申请</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-kotlin" data-lang="kotlin"><span style="color:#66d9ef">package</span> top.kikt.camerax.scopedstorage

<span style="color:#66d9ef">import</span> android.Manifest
<span style="color:#66d9ef">import</span> android.database.Cursor
<span style="color:#66d9ef">import</span> android.graphics.Bitmap
<span style="color:#66d9ef">import</span> android.net.Uri
<span style="color:#66d9ef">import</span> android.os.Build
<span style="color:#66d9ef">import</span> android.os.Bundle
<span style="color:#66d9ef">import</span> android.provider.MediaStore
<span style="color:#66d9ef">import</span> android.util.Log
<span style="color:#66d9ef">import</span> android.util.Size
<span style="color:#66d9ef">import</span> androidx.appcompat.app.AppCompatActivity
<span style="color:#66d9ef">import</span> androidx.core.database.getIntOrNull
<span style="color:#66d9ef">import</span> com.tbruyelle.rxpermissions2.RxPermissions
<span style="color:#66d9ef">import</span> kotlinx.android.synthetic.main.activity_media_scan.*

<span style="color:#66d9ef">class</span> <span style="color:#a6e22e">MediaScanActivity</span> : AppCompatActivity() {

    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">val</span> rxPermissions = RxPermissions(<span style="color:#66d9ef">this</span>)

    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">val</span> TAG = <span style="color:#e6db74">&#34;MediaScanActivity&#34;</span>

    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">val</span> storeImageKeys = arrayOf(
        MediaStore.Images.Media.DISPLAY_NAME, <span style="color:#75715e">// 显示的名字
</span><span style="color:#75715e"></span>        MediaStore.Images.Media.DATA, <span style="color:#75715e">// 数据
</span><span style="color:#75715e"></span>        MediaStore.Images.Media.LONGITUDE, <span style="color:#75715e">// 经度
</span><span style="color:#75715e"></span>        MediaStore.Images.Media._ID, <span style="color:#75715e">// id
</span><span style="color:#75715e"></span>        MediaStore.Images.Media.MINI_THUMB_MAGIC, <span style="color:#75715e">// id
</span><span style="color:#75715e"></span>        MediaStore.Images.Media.TITLE, <span style="color:#75715e">// id
</span><span style="color:#75715e"></span>        MediaStore.Images.Media.BUCKET_ID, <span style="color:#75715e">// dir id 目录
</span><span style="color:#75715e"></span>        MediaStore.Images.Media.BUCKET_DISPLAY_NAME, <span style="color:#75715e">// dir name 目录名字
</span><span style="color:#75715e">//        MediaStore.Images.Media.EXTERNAL_CONTENT_URI, // dir name 目录名字
</span><span style="color:#75715e"></span>        MediaStore.Images.Media.WIDTH, <span style="color:#75715e">// 宽
</span><span style="color:#75715e"></span>        MediaStore.Images.Media.HEIGHT, <span style="color:#75715e">// 高
</span><span style="color:#75715e"></span>        MediaStore.Images.Media.DATE_TAKEN <span style="color:#75715e">//日期
</span><span style="color:#75715e"></span>    )

    <span style="color:#66d9ef">override</span> <span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">onCreate</span>(savedInstanceState: Bundle?) {
        <span style="color:#66d9ef">super</span>.onCreate(savedInstanceState)
        setContentView(R.layout.activity_media_scan)

        bt_scan.setOnClickListener {
            Log.d(TAG, <span style="color:#e6db74">&#34;准备申请权限&#34;</span>)
            rxPermissions.request(
                Manifest.permission.WRITE_EXTERNAL_STORAGE,
                Manifest.permission.READ_EXTERNAL_STORAGE
            )
                .subscribe {
                    <span style="color:#66d9ef">if</span> (it) {
                        Log.d(TAG, <span style="color:#e6db74">&#34;申请权限成功&#34;</span>)
                        scan()
                    } <span style="color:#66d9ef">else</span> {
                        Log.d(TAG, <span style="color:#e6db74">&#34;申请失败&#34;</span>)
                    }
                }
        }
    }

    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">scan</span>() {
        <span style="color:#66d9ef">val</span> cursor = contentResolver.query(
            MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
            storeImageKeys,
            <span style="color:#66d9ef">null</span>,
            <span style="color:#66d9ef">null</span>,
            MediaStore.Images.Media.DATE_TAKEN
        )

        cursor<span style="color:#f92672">?.</span>apply {
            <span style="color:#66d9ef">val</span> count = <span style="color:#66d9ef">this</span>.count
            Log.d(TAG, <span style="color:#e6db74">&#34;scan count is $count&#34;</span>)
            <span style="color:#66d9ef">while</span> (<span style="color:#66d9ef">this</span>.moveToNext()) {
                <span style="color:#66d9ef">val</span> date = <span style="color:#66d9ef">this</span>.getString(MediaStore.Images.Media.DATA)
                Log.d(TAG, <span style="color:#e6db74">&#34;path : $date&#34;</span>)
<span style="color:#75715e">//                contentResolver.loadThumbnail()
</span><span style="color:#75715e"></span>                <span style="color:#66d9ef">val</span> width = <span style="color:#66d9ef">this</span>.getInteger(MediaStore.Images.Media.WIDTH) <span style="color:#f92672">?:</span> <span style="color:#ae81ff">1024</span>
                <span style="color:#66d9ef">val</span> height = <span style="color:#66d9ef">this</span>.getInteger(MediaStore.Images.Media.HEIGHT) <span style="color:#f92672">?:</span> <span style="color:#ae81ff">1024</span>
                Log.d(TAG, <span style="color:#e6db74">&#34;width : $width&#34;</span>)
                Log.d(TAG, <span style="color:#e6db74">&#34;height : $height&#34;</span>)

                <span style="color:#66d9ef">var</span> photoUri = Uri.withAppendedPath(
                    MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
                    cursor.getString(MediaStore.Images.Media._ID)
                )

                Log.d(TAG, <span style="color:#e6db74">&#34;version int = ${Build.VERSION.SDK_INT}&#34;</span>)

<span style="color:#75715e">//                if (Build.VERSION.SDK_INT &gt;= Build.VERSION_CODES.Q) {
</span><span style="color:#75715e"></span>                    photoUri = MediaStore.setRequireOriginal(photoUri)
<span style="color:#75715e">//                    val stream = contentResolver.openInputStream(photoUri)
</span><span style="color:#75715e"></span>                    <span style="color:#66d9ef">val</span> bitmap = contentResolver.loadThumbnail(photoUri, Size(width, height), <span style="color:#66d9ef">null</span>)
                    iv_preview.setImageBitmap(bitmap)
<span style="color:#75715e">//                }
</span><span style="color:#75715e"></span>            }
        }

        cursor<span style="color:#f92672">?.</span>close()
    }

    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">Cursor</span>.getString(columnName: String): String? {
        <span style="color:#66d9ef">val</span> columnIndex = getColumnIndex(columnName)
        <span style="color:#66d9ef">if</span> (columnIndex == -<span style="color:#ae81ff">1</span>) {
            <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">null</span>
        }
        <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">this</span>.getString(columnIndex)
    }

    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">Cursor</span>.getInteger(columnName: String): Int? {
        <span style="color:#66d9ef">val</span> columnIndex = getColumnIndex(columnName)
        <span style="color:#66d9ef">if</span> (columnIndex == -<span style="color:#ae81ff">1</span>) {
            <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">null</span>
        }
        <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">this</span>.getIntOrNull(columnIndex)
    }

    <span style="color:#66d9ef">data</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">ImageEntity</span>(<span style="color:#66d9ef">val</span> width: Int, <span style="color:#66d9ef">val</span> height: Int, <span style="color:#66d9ef">val</span> bitmap: Bitmap) {
        <span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">dispose</span>() {
            bitmap.recycle()
        }
    }
}

</code></pre></div><p>布局在这里</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-xml" data-lang="xml"><span style="color:#75715e">&lt;?xml version=&#34;1.0&#34; encoding=&#34;utf-8&#34;?&gt;</span>
<span style="color:#f92672">&lt;androidx.constraintlayout.widget.ConstraintLayout</span>
        <span style="color:#a6e22e">xmlns:android=</span><span style="color:#e6db74">&#34;http://schemas.android.com/apk/res/android&#34;</span>
        <span style="color:#a6e22e">xmlns:tools=</span><span style="color:#e6db74">&#34;http://schemas.android.com/tools&#34;</span>
        <span style="color:#a6e22e">xmlns:app=</span><span style="color:#e6db74">&#34;http://schemas.android.com/apk/res-auto&#34;</span>
        <span style="color:#a6e22e">android:layout_width=</span><span style="color:#e6db74">&#34;match_parent&#34;</span>
        <span style="color:#a6e22e">android:layout_height=</span><span style="color:#e6db74">&#34;match_parent&#34;</span>
        <span style="color:#a6e22e">tools:context=</span><span style="color:#e6db74">&#34;.MediaScanActivity&#34;</span><span style="color:#f92672">&gt;</span>

    <span style="color:#f92672">&lt;Button</span>
            <span style="color:#a6e22e">android:text=</span><span style="color:#e6db74">&#34;扫描&#34;</span>
            <span style="color:#a6e22e">android:layout_width=</span><span style="color:#e6db74">&#34;wrap_content&#34;</span>
            <span style="color:#a6e22e">android:layout_height=</span><span style="color:#e6db74">&#34;wrap_content&#34;</span> <span style="color:#a6e22e">tools:layout_editor_absoluteY=</span><span style="color:#e6db74">&#34;16dp&#34;</span>
            <span style="color:#a6e22e">tools:layout_editor_absoluteX=</span><span style="color:#e6db74">&#34;16dp&#34;</span> <span style="color:#a6e22e">android:id=</span><span style="color:#e6db74">&#34;@+id/bt_scan&#34;</span><span style="color:#f92672">/&gt;</span>
    <span style="color:#f92672">&lt;ImageView</span>
            <span style="color:#a6e22e">android:layout_width=</span><span style="color:#e6db74">&#34;0dp&#34;</span>
            <span style="color:#a6e22e">android:layout_height=</span><span style="color:#e6db74">&#34;0dp&#34;</span>
            <span style="color:#a6e22e">android:id=</span><span style="color:#e6db74">&#34;@+id/iv_preview&#34;</span> <span style="color:#a6e22e">app:layout_constraintEnd_toEndOf=</span><span style="color:#e6db74">&#34;parent&#34;</span>
            <span style="color:#a6e22e">app:layout_constraintStart_toStartOf=</span><span style="color:#e6db74">&#34;parent&#34;</span> <span style="color:#a6e22e">android:layout_marginStart=</span><span style="color:#e6db74">&#34;8dp&#34;</span>
            <span style="color:#a6e22e">app:layout_constraintBottom_toBottomOf=</span><span style="color:#e6db74">&#34;parent&#34;</span> <span style="color:#a6e22e">android:layout_marginTop=</span><span style="color:#e6db74">&#34;8dp&#34;</span>
            <span style="color:#a6e22e">app:layout_constraintTop_toBottomOf=</span><span style="color:#e6db74">&#34;@+id/bt_scan&#34;</span> <span style="color:#a6e22e">android:layout_marginEnd=</span><span style="color:#e6db74">&#34;8dp&#34;</span>
            <span style="color:#a6e22e">android:layout_marginBottom=</span><span style="color:#e6db74">&#34;8dp&#34;</span><span style="color:#f92672">/&gt;</span>
<span style="color:#f92672">&lt;/androidx.constraintlayout.widget.ConstraintLayout&gt;</span>
</code></pre></div><p>点击扫描后能看见这个图片
<img src="https://dev.azure.com/cjlspy/844861b8-ee45-48a3-b430-b974684baaaa/_apis/git/repositories/bf4da1c1-1cfa-4f54-8a75-745cce2b737c/items?versionDescriptor%5Bversion%5D=master&amp;path=%2F20190520155614.png&amp;versionDescriptor%5BversionOptions%5D=0&amp;versionDescriptor%5BversionType%5D=0&amp;resolveLfs=true&amp;%24format=octetStream&amp;api-version=5.0" alt="20190520155614.png"></p>
<p>相册里只有这一张图</p>
<p><img src="https://dev.azure.com/cjlspy/844861b8-ee45-48a3-b430-b974684baaaa/_apis/git/repositories/bf4da1c1-1cfa-4f54-8a75-745cce2b737c/items?versionDescriptor%5Bversion%5D=master&amp;path=%2F20190520155645.png&amp;versionDescriptor%5BversionOptions%5D=0&amp;versionDescriptor%5BversionType%5D=0&amp;resolveLfs=true&amp;%24format=octetStream&amp;api-version=5.0" alt="20190520155645.png"></p>
<p>核心代码如下:</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-kotlin" data-lang="kotlin">    <span style="color:#66d9ef">val</span> cursor = contentResolver.query(
        MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
        storeImageKeys,
        <span style="color:#66d9ef">null</span>,
        <span style="color:#66d9ef">null</span>,
        MediaStore.Images.Media.DATE_TAKEN
    )

    cursor<span style="color:#f92672">?.</span>apply {
        <span style="color:#66d9ef">val</span> count = <span style="color:#66d9ef">this</span>.count
        Log.d(TAG, <span style="color:#e6db74">&#34;scan count is $count&#34;</span>)
        <span style="color:#66d9ef">while</span> (<span style="color:#66d9ef">this</span>.moveToNext()) {
            <span style="color:#66d9ef">val</span> date = <span style="color:#66d9ef">this</span>.getString(MediaStore.Images.Media.DATA)
            Log.d(TAG, <span style="color:#e6db74">&#34;path : $date&#34;</span>)
<span style="color:#75715e">//                contentResolver.loadThumbnail()
</span><span style="color:#75715e"></span>            <span style="color:#66d9ef">val</span> width = <span style="color:#66d9ef">this</span>.getInteger(MediaStore.Images.Media.WIDTH) <span style="color:#f92672">?:</span> <span style="color:#ae81ff">1024</span>
            <span style="color:#66d9ef">val</span> height = <span style="color:#66d9ef">this</span>.getInteger(MediaStore.Images.Media.HEIGHT) <span style="color:#f92672">?:</span> <span style="color:#ae81ff">1024</span>
            Log.d(TAG, <span style="color:#e6db74">&#34;width : $width&#34;</span>)
            Log.d(TAG, <span style="color:#e6db74">&#34;height : $height&#34;</span>)

            <span style="color:#66d9ef">var</span> photoUri = Uri.withAppendedPath(
                MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
                cursor.getString(MediaStore.Images.Media._ID)
            )

            Log.d(TAG, <span style="color:#e6db74">&#34;version int = ${Build.VERSION.SDK_INT}&#34;</span>)

<span style="color:#75715e">//                if (Build.VERSION.SDK_INT &gt;= Build.VERSION_CODES.Q) { // 这里注释的原因是因为模拟器中获取的是28, 而SDK中定义是10000, 这样写这个代码跑不起来
</span><span style="color:#75715e"></span>                photoUri = MediaStore.setRequireOriginal(photoUri)
<span style="color:#75715e">//                    val stream = contentResolver.openInputStream(photoUri)
</span><span style="color:#75715e"></span>                <span style="color:#66d9ef">val</span> bitmap = contentResolver.loadThumbnail(photoUri, Size(width, height), <span style="color:#66d9ef">null</span>)
                iv_preview.setImageBitmap(bitmap)
<span style="color:#75715e">//                }
</span><span style="color:#75715e"></span>        }
    }

    cursor<span style="color:#f92672">?.</span>close()

</code></pre></div><p>以前版本直接用 Data 就可以拿到图片对应的 File path</p>
<p>而 android-Q 以后,不能再这样做了,需要通过 id 来&quot;组装&quot;出一个 Uri, 然后通过 <code>contentResolver.loadThumbnail()</code>来获取缩略图</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-kotlin" data-lang="kotlin">    <span style="color:#66d9ef">var</span> photoUri = Uri.withAppendedPath(
        MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
        cursor.getString(MediaStore.Images.Media._ID)
    )

    Log.d(TAG, <span style="color:#e6db74">&#34;version int = ${Build.VERSION.SDK_INT}&#34;</span>)

<span style="color:#75715e">//                if (Build.VERSION.SDK_INT &gt;= Build.VERSION_CODES.Q) {
</span><span style="color:#75715e"></span>    photoUri = MediaStore.setRequireOriginal(photoUri)
<span style="color:#75715e">//                    val stream = contentResolver.openInputStream(photoUri)
</span><span style="color:#75715e"></span>    <span style="color:#66d9ef">val</span> bitmap = contentResolver.loadThumbnail(photoUri, Size(width, height), <span style="color:#66d9ef">null</span>)
    iv_preview.setImageBitmap(bitmap)
</code></pre></div><h4 id="需要用-jni-处理图片">需要用 jni 处理图片</h4>
<p>有的时候,你需要使用 ndk/jni 来处理文件,这个时候,可以用 openFileDescriptor 来完成这个事情</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-kotlin" data-lang="kotlin"><span style="color:#66d9ef">val</span> fileOpenMode = <span style="color:#e6db74">&#34;r&#34;</span>
<span style="color:#66d9ef">val</span> parcelFd = resolver.openFileDescriptor(photoUri, fileOpenMode)
<span style="color:#66d9ef">val</span> fd = parcelFd<span style="color:#f92672">?.</span>detachFd()
</code></pre></div><h4 id="修改其他项目创建的文件">修改其他项目创建的文件</h4>
<p>需要捕获<code>RecoverableSecurityException</code>异常,并请求用户允许修改</p>
<p>官方原文如下:</p>
<blockquote>
<p>Update other apps&rsquo; media files
Note: Expect the following behavior to take effect in a future beta release of Android Q.
To modify a given media file that another app originally saved to an external storage device, catch the RecoverableSecurityException that the platform throws. You can then request that the user grant your app write access to that specific item.</p>
</blockquote>
<h4 id="照片的位置信息">照片的位置信息</h4>
<p>一些照片在 Exif 中包含位置的敏感信息, 以前的话可以直接通过 MediaStore 的 query 方法从数据库中查到, 现在就不再可以了</p>
<p>如果需要这些元数据,需要请求用户同意, 需要<code>ACCESS_MEDIA_LOCATION</code>权限, 这个是一个 android-Q 的新权限</p>
<p>并调用<code>setRequireOriginal()</code>方法来获取对应的 Uri</p>
<p>官方给的示例如下:</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-kotlin" data-lang="kotlin"><span style="color:#75715e">// Get location data from the ExifInterface class.
</span><span style="color:#75715e"></span><span style="color:#66d9ef">val</span> photoUri = MediaStore.setRequireOriginal(photoUri)
contentResolver.openInputStream(photoUri).use { stream -&gt;
    ExifInterface(stream).run {
        <span style="color:#75715e">// If lat/long is null, fall back to the coordinates (0, 0).
</span><span style="color:#75715e"></span>        <span style="color:#66d9ef">val</span> latLong = <span style="color:#f92672">?:</span> doubleArrayOf(<span style="color:#ae81ff">0.0</span>, <span style="color:#ae81ff">0.0</span>)
    }
}
</code></pre></div><p>可是我这里有报错的地方,我修改如下:</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-kotlin" data-lang="kotlin">contentResolver.openInputStream(photoUri).use { stream -&gt;
    ExifInterface(stream).run {
        <span style="color:#66d9ef">val</span> floatArrayOf = floatArrayOf(<span style="color:#ae81ff">0f</span>, <span style="color:#ae81ff">0f</span>)
        <span style="color:#66d9ef">val</span> latLongResult = <span style="color:#66d9ef">this</span>.getLatLong(floatArrayOf)
        Log.d(TAG, <span style="color:#e6db74">&#34;latLng request $latLongResult latlng = ${floatArrayOf.toList()}&#34;</span>)
    }
}
</code></pre></div><p>在加入权限的请求后我能够得到一个日志, 告诉我照片的经纬度</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-dart" data-lang="dart"><span style="color:#ae81ff">2019</span><span style="color:#f92672">-</span><span style="color:#ae81ff">05</span><span style="color:#f92672">-</span><span style="color:#ae81ff">20</span> <span style="color:#ae81ff">17</span><span style="color:#f92672">:</span><span style="color:#ae81ff">02</span><span style="color:#f92672">:</span><span style="color:#ae81ff">01.436</span> <span style="color:#ae81ff">19236</span><span style="color:#f92672">-</span><span style="color:#ae81ff">19236</span><span style="color:#f92672">/</span>top.kikt.camerax.scopedstorage D<span style="color:#f92672">/</span>MediaScanActivity: latLng request <span style="color:#66d9ef">true</span> latlng <span style="color:#f92672">=</span> [<span style="color:#ae81ff">39.841603</span>, <span style="color:#ae81ff">116.317417</span>],
</code></pre></div><h2 id="sdcard-目录结构">sdcard 目录结构</h2>
<p>系统自带的 files 无法完整的看到目录结构, 我考虑使用 adb shell 命令来查看</p>
<div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">generic_x86:/sdcard $ ls
Alarms  DCIM     Movies Notifications Podcasts
Android Download Music  Pictures      Ringtones
</code></pre></div><div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">generic_x86:/sdcard $ cd DCIM
generic_x86:/sdcard/DCIM $ ls
Camera
generic_x86:/sdcard/DCIM $ ls Camera/
IMG_20190518_232111.jpg IMG_20190520_140142.jpg
generic_x86:/sdcard/DCIM $
</code></pre></div><div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">cd /sdcard/Android <span style="color:#75715e"># 这一步应该是常规的数据</span>
ls data/
com.android.calllogbackup         com.android.phone                    com.android.service.ims.presence      com.google.android.packageinstaller
com.android.camera2               com.android.printspooler             com.android.smspush                   com.google.android.partnersetup
com.android.carrierconfig         com.android.providers.blockednumber  com.android.stk                       com.google.android.sdksetup
com.android.cellbroadcastreceiver com.android.providers.calendar       com.android.vending                   com.google.android.setupwizard
com.android.chrome                com.android.providers.contacts       com.google.android.apps.docs          com.google.android.storagemanager
com.android.managedprovisioning   com.android.providers.telephony      com.google.android.apps.maps          com.google.android.videos
com.android.mms.service           com.android.providers.userdictionary com.google.android.gms                com.google.android.webview
com.android.nfc                   com.android.se                       com.google.android.gsf                com.google.android.youtube
com.android.ons                   com.android.service.ims              com.google.android.onetimeinitializer top.kikt.camerax.scopedstorage
</code></pre></div><div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">130|generic_x86:/sdcard/Android $ ls /sdcard/Android/data/top.kikt.camerax.scopedstorage/
cache
generic_x86:/sdcard/Android $ ls /sdcard/Android/data/top.kikt.camerax.scopedstorage/cache/
abc.txt <span style="color:#75715e"># 这个是代码第一步创建的那个文件</span>
</code></pre></div><div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">generic_x86:/sdcard/Android $ ls /sdcard/Android/media
com.android.calllogbackup         com.android.phone                    com.android.service.ims          com.google.android.onetimeinitializer
com.android.carrierconfig         com.android.printspooler             com.android.service.ims.presence com.google.android.packageinstaller
com.android.cellbroadcastreceiver com.android.providers.blockednumber  com.android.smspush              com.google.android.partnersetup
com.android.chrome                com.android.providers.calendar       com.android.stk                  com.google.android.sdksetup
com.android.managedprovisioning   com.android.providers.contacts       com.android.vending              com.google.android.setupwizard
com.android.mms.service           com.android.providers.telephony      com.google.android.apps.maps     com.google.android.storagemanager
com.android.nfc                   com.android.providers.userdictionary com.google.android.gms           com.google.android.webview
com.android.ons                   com.android.se                       com.google.android.gsf           top.kikt.camerax.scopedstorage
</code></pre></div><p>这一步的话,应该是创建可以在卸载 app 后依然会保留的 media 部分</p>
<p>基本和前面说的沙箱,本应用的 media, 系统相册等进行了一一对应</p>
<h2 id="后记">后记</h2>
<p><a href="https://github.com/CaiJingLong/android-q-privacy-example">代码在这</a></p>
<p>以上</p>

                        </div>

                        

<div class="post-archive">
    <h2>See Also</h2>
    <ul class="listing">
        
        <li><a href="/posts/android/camera/camerax-usage/">CameraX的使用</a></li>
        
        <li><a href="/posts/flutter/android/profile-1/">在Profile下分析android内存占用</a></li>
        
        <li><a href="/posts/flutter/exists/android/">在原 Android 工程的基础上构建一个flutter工程</a></li>
        
        <li><a href="/posts/android/android-include-ffmpeg/">android 引入ffmpeg</a></li>
        
        <li><a href="/posts/android/android-ffmpeg-compile-so/">ndk编译ffmpeg</a></li>
        
    </ul>
</div>


                        <div class="post-meta meta-tags">
                            
                            <ul class="clearfix">
                                
                                <li><a href="http://www.kikt.top/tags/android">android</a></li>
                                
                                <li><a href="http://www.kikt.top/tags/androidQ">androidQ</a></li>
                                
                                <li><a href="http://www.kikt.top/tags/%E9%9A%90%E7%A7%81">隐私</a></li>
                                
                                <li><a href="http://www.kikt.top/tags/10">10</a></li>
                                
                            </ul>
                            
                        </div>
                    </article>
                    
    

    
    
    <div class="post bg-white">
      <script src="https://utteranc.es/client.js"
            repo= "caijinglong/kikt-blog-comment"
            issue-term="pathname"
            theme="github-light"
            crossorigin="anonymous"
            async>
      </script>
    </div>
    
                </div>
            </div>
            <div id="secondary">
    <section class="widget">
        <form id="search" action="//www.google.com/search" method="get" accept-charset="utf-8" target="_blank" _lpchecked="1">
      
      <input type="text" name="q" maxlength="20" placeholder="Search">
      <input type="hidden" name="sitesearch" value="http://www.kikt.top">
      <button type="submit" class="submit icon-search"></button>
</form>
    </section>
    
    <section class="widget">
        <h3 class="widget-title">最近文章</h3>
<ul class="widget-list">
    
    <li>
        <a href="http://www.kikt.top/posts/java/crack/crack1/" title="使用javassist,修改jar包方法实现">使用javassist,修改jar包方法实现</a>
    </li>
    
    <li>
        <a href="http://www.kikt.top/posts/github/actions/create/" title="Github action 的开发到发布">Github action 的开发到发布</a>
    </li>
    
    <li>
        <a href="http://www.kikt.top/posts/flutter/jenkins&#43;fastlane/" title="flutter Jenkins&#43;fastlane 自动化打测试包, 并上传蒲公英">flutter Jenkins&#43;fastlane 自动化打测试包, 并上传蒲公英</a>
    </li>
    
    <li>
        <a href="http://www.kikt.top/posts/go/compile-for-android/" title="编译 go 源码为 android 动态库(so)">编译 go 源码为 android 动态库(so)</a>
    </li>
    
    <li>
        <a href="http://www.kikt.top/posts/flutter/focusnode-2/" title="Flutter FocusNode 焦点那点事-(二)">Flutter FocusNode 焦点那点事-(二)</a>
    </li>
    
    <li>
        <a href="http://www.kikt.top/posts/flutter/focusnode-1/" title="Flutter FocusNode 焦点那点事-(一)">Flutter FocusNode 焦点那点事-(一)</a>
    </li>
    
    <li>
        <a href="http://www.kikt.top/posts/server/caddy/cros_for_local_dev/" title="用 Caddy 解决 web 开发中本地跨域的问题">用 Caddy 解决 web 开发中本地跨域的问题</a>
    </li>
    
    <li>
        <a href="http://www.kikt.top/posts/flutter/route/navigator-helper2/" title="Navigator Helper2">Navigator Helper2</a>
    </li>
    
    <li>
        <a href="http://www.kikt.top/posts/flutter/grey-app/" title="flutter 怎么实现app整体灰度">flutter 怎么实现app整体灰度</a>
    </li>
    
    <li>
        <a href="http://www.kikt.top/posts/flutter/plugin/flutter-sdk-import-aar/" title="Flutter 插件开发之引入aar到安卓部分 并使用本地maven">Flutter 插件开发之引入aar到安卓部分 并使用本地maven</a>
    </li>
    
</ul>
    </section>

    

    <section class="widget">
        <h3 class="widget-title">分类</h3>
<ul class="widget-list">
    
    <li>
        <a href="http://www.kikt.top/categories/android/">android(25)</a>
    </li>
    
    <li>
        <a href="http://www.kikt.top/categories/caddy/">caddy(1)</a>
    </li>
    
    <li>
        <a href="http://www.kikt.top/categories/dart/">dart(5)</a>
    </li>
    
    <li>
        <a href="http://www.kikt.top/categories/docker/">docker(3)</a>
    </li>
    
    <li>
        <a href="http://www.kikt.top/categories/flutter/">flutter(60)</a>
    </li>
    
    <li>
        <a href="http://www.kikt.top/categories/github/">github(3)</a>
    </li>
    
    <li>
        <a href="http://www.kikt.top/categories/go/">go(1)</a>
    </li>
    
    <li>
        <a href="http://www.kikt.top/categories/golang/">golang(1)</a>
    </li>
    
    <li>
        <a href="http://www.kikt.top/categories/iOS/">iOS(6)</a>
    </li>
    
    <li>
        <a href="http://www.kikt.top/categories/ios/">ios(1)</a>
    </li>
    
    <li>
        <a href="http://www.kikt.top/categories/java/">java(6)</a>
    </li>
    
    <li>
        <a href="http://www.kikt.top/categories/jetbrains/">jetbrains(1)</a>
    </li>
    
    <li>
        <a href="http://www.kikt.top/categories/other/">other(3)</a>
    </li>
    
    <li>
        <a href="http://www.kikt.top/categories/python/">python(1)</a>
    </li>
    
    <li>
        <a href="http://www.kikt.top/categories/server/">server(5)</a>
    </li>
    
    <li>
        <a href="http://www.kikt.top/categories/spring/">spring(3)</a>
    </li>
    
    <li>
        <a href="http://www.kikt.top/categories/tools/">tools(1)</a>
    </li>
    
    <li>
        <a href="http://www.kikt.top/categories/%E6%9D%82%E9%A1%B9/">杂项(1)</a>
    </li>
    
    <li>
        <a href="http://www.kikt.top/categories/%E6%B6%82%E9%B8%A6/">涂鸦(1)</a>
    </li>
    
</ul>
    </section>

    <section class="widget">
        <h3 class="widget-title">标签</h3>
<div class="tagcloud">
    
    <a href="http://www.kikt.top/tags/10/">10</a>
    
    <a href="http://www.kikt.top/tags/2.3/">2.3</a>
    
    <a href="http://www.kikt.top/tags/Clipboard/">Clipboard</a>
    
    <a href="http://www.kikt.top/tags/Javassist/">Javassist</a>
    
    <a href="http://www.kikt.top/tags/Migrate/">Migrate</a>
    
    <a href="http://www.kikt.top/tags/aar/">aar</a>
    
    <a href="http://www.kikt.top/tags/actions/">actions</a>
    
    <a href="http://www.kikt.top/tags/aidl/">aidl</a>
    
    <a href="http://www.kikt.top/tags/android/">android</a>
    
    <a href="http://www.kikt.top/tags/androidQ/">androidQ</a>
    
    <a href="http://www.kikt.top/tags/androidX/">androidX</a>
    
    <a href="http://www.kikt.top/tags/apk/">apk</a>
    
    <a href="http://www.kikt.top/tags/app/">app</a>
    
    <a href="http://www.kikt.top/tags/appcode/">appcode</a>
    
    <a href="http://www.kikt.top/tags/azure/">azure</a>
    
    <a href="http://www.kikt.top/tags/ble/">ble</a>
    
    <a href="http://www.kikt.top/tags/bottomsheet/">bottomsheet</a>
    
    <a href="http://www.kikt.top/tags/butterknife/">butterknife</a>
    
    <a href="http://www.kikt.top/tags/c/">c</a>
    
    <a href="http://www.kikt.top/tags/caddy/">caddy</a>
    
    <a href="http://www.kikt.top/tags/camera/">camera</a>
    
    <a href="http://www.kikt.top/tags/cameraX/">cameraX</a>
    
    <a href="http://www.kikt.top/tags/channel/">channel</a>
    
    <a href="http://www.kikt.top/tags/cocoapods/">cocoapods</a>
    
    <a href="http://www.kikt.top/tags/cupertino/">cupertino</a>
    
    <a href="http://www.kikt.top/tags/dart/">dart</a>
    
    <a href="http://www.kikt.top/tags/dart-2.6/">dart-2.6</a>
    
    <a href="http://www.kikt.top/tags/desktop/">desktop</a>
    
    <a href="http://www.kikt.top/tags/dialog/">dialog</a>
    
    <a href="http://www.kikt.top/tags/dmg/">dmg</a>
    
    <a href="http://www.kikt.top/tags/doc/">doc</a>
    
    <a href="http://www.kikt.top/tags/docker/">docker</a>
    
    <a href="http://www.kikt.top/tags/docker-compose/">docker-compose</a>
    
    <a href="http://www.kikt.top/tags/excel/">excel</a>
    
    <a href="http://www.kikt.top/tags/exists/">exists</a>
    
    <a href="http://www.kikt.top/tags/faq/">faq</a>
    
    <a href="http://www.kikt.top/tags/fastlane/">fastlane</a>
    
    <a href="http://www.kikt.top/tags/ffi/">ffi</a>
    
    <a href="http://www.kikt.top/tags/ffmpeg/">ffmpeg</a>
    
    <a href="http://www.kikt.top/tags/flexmark/">flexmark</a>
    
    <a href="http://www.kikt.top/tags/flutter/">flutter</a>
    
    <a href="http://www.kikt.top/tags/flutter-web/">flutter-web</a>
    
    <a href="http://www.kikt.top/tags/focus/">focus</a>
    
    <a href="http://www.kikt.top/tags/focusNode/">focusNode</a>
    
    <a href="http://www.kikt.top/tags/form/">form</a>
    
    <a href="http://www.kikt.top/tags/framework/">framework</a>
    
    <a href="http://www.kikt.top/tags/github/">github</a>
    
    <a href="http://www.kikt.top/tags/go/">go</a>
    
    <a href="http://www.kikt.top/tags/golang/">golang</a>
    
    <a href="http://www.kikt.top/tags/gradle/">gradle</a>
    
    <a href="http://www.kikt.top/tags/http/">http</a>
    
    <a href="http://www.kikt.top/tags/iOS/">iOS</a>
    
    <a href="http://www.kikt.top/tags/ide/">ide</a>
    
    <a href="http://www.kikt.top/tags/idea/">idea</a>
    
    <a href="http://www.kikt.top/tags/index/">index</a>
    
    <a href="http://www.kikt.top/tags/inside/">inside</a>
    
    <a href="http://www.kikt.top/tags/ios/">ios</a>
    
    <a href="http://www.kikt.top/tags/java/">java</a>
    
    <a href="http://www.kikt.top/tags/jenkins/">jenkins</a>
    
    <a href="http://www.kikt.top/tags/jetbrains/">jetbrains</a>
    
    <a href="http://www.kikt.top/tags/jsdelivr/">jsdelivr</a>
    
    <a href="http://www.kikt.top/tags/json/">json</a>
    
    <a href="http://www.kikt.top/tags/json_serializable/">json_serializable</a>
    
    <a href="http://www.kikt.top/tags/junit/">junit</a>
    
    <a href="http://www.kikt.top/tags/library/">library</a>
    
    <a href="http://www.kikt.top/tags/live-template/">live-template</a>
    
    <a href="http://www.kikt.top/tags/loadmore/">loadmore</a>
    
    <a href="http://www.kikt.top/tags/log/">log</a>
    
    <a href="http://www.kikt.top/tags/mac/">mac</a>
    
    <a href="http://www.kikt.top/tags/macos/">macos</a>
    
    <a href="http://www.kikt.top/tags/maven/">maven</a>
    
    <a href="http://www.kikt.top/tags/mysql/">mysql</a>
    
    <a href="http://www.kikt.top/tags/navigationbar/">navigationbar</a>
    
    <a href="http://www.kikt.top/tags/navigator/">navigator</a>
    
    <a href="http://www.kikt.top/tags/ndk/">ndk</a>
    
    <a href="http://www.kikt.top/tags/nginx/">nginx</a>
    
    <a href="http://www.kikt.top/tags/oc/">oc</a>
    
    <a href="http://www.kikt.top/tags/opencv/">opencv</a>
    
    <a href="http://www.kikt.top/tags/poi/">poi</a>
    
    <a href="http://www.kikt.top/tags/pub/">pub</a>
    
    <a href="http://www.kikt.top/tags/python/">python</a>
    
    <a href="http://www.kikt.top/tags/retrofit/">retrofit</a>
    
    <a href="http://www.kikt.top/tags/route/">route</a>
    
    <a href="http://www.kikt.top/tags/server/">server</a>
    
    <a href="http://www.kikt.top/tags/so/">so</a>
    
    <a href="http://www.kikt.top/tags/spp/">spp</a>
    
    <a href="http://www.kikt.top/tags/sprintboot/">sprintboot</a>
    
    <a href="http://www.kikt.top/tags/swift/">swift</a>
    
    <a href="http://www.kikt.top/tags/tap/">tap</a>
    
    <a href="http://www.kikt.top/tags/thymeleaf/">thymeleaf</a>
    
    <a href="http://www.kikt.top/tags/tips/">tips</a>
    
    <a href="http://www.kikt.top/tags/toast/">toast</a>
    
    <a href="http://www.kikt.top/tags/transition/">transition</a>
    
    <a href="http://www.kikt.top/tags/ui/">ui</a>
    
    <a href="http://www.kikt.top/tags/utf8/">utf8</a>
    
    <a href="http://www.kikt.top/tags/yield/">yield</a>
    
    <a href="http://www.kikt.top/tags/%E4%BA%8C%E8%BF%9B%E5%88%B6/">二进制</a>
    
    <a href="http://www.kikt.top/tags/%E4%BA%A4%E5%8F%89/">交叉</a>
    
    <a href="http://www.kikt.top/tags/%E4%BA%A4%E5%8F%89%E7%BC%96%E8%AF%91/">交叉编译</a>
    
    <a href="http://www.kikt.top/tags/%E4%BB%8B%E7%BB%8D/">介绍</a>
    
    <a href="http://www.kikt.top/tags/%E4%BB%A3%E7%A0%81%E8%A7%84%E8%8C%83/">代码规范</a>
    
    <a href="http://www.kikt.top/tags/%E5%86%B2%E7%AA%81/">冲突</a>
    
    <a href="http://www.kikt.top/tags/%E5%8A%A8%E6%80%81/">动态</a>
    
    <a href="http://www.kikt.top/tags/%E5%8F%91%E5%B8%83/">发布</a>
    
    <a href="http://www.kikt.top/tags/%E5%8F%AF%E6%89%A7%E8%A1%8C/">可执行</a>
    
    <a href="http://www.kikt.top/tags/%E5%9B%BE%E5%BA%8A/">图床</a>
    
    <a href="http://www.kikt.top/tags/%E5%9F%8E%E5%B8%82/">城市</a>
    
    <a href="http://www.kikt.top/tags/%E5%A4%A7%E6%96%87%E4%BB%B6/">大文件</a>
    
    <a href="http://www.kikt.top/tags/%E5%B7%A5%E5%85%B7/">工具</a>
    
    <a href="http://www.kikt.top/tags/%E5%B7%B2%E6%9C%89%E9%A1%B9%E7%9B%AE/">已有项目</a>
    
    <a href="http://www.kikt.top/tags/%E5%BC%80%E5%8F%91%E7%8E%AF%E5%A2%83/">开发环境</a>
    
    <a href="http://www.kikt.top/tags/%E6%80%BB%E7%BB%93/">总结</a>
    
    <a href="http://www.kikt.top/tags/%E6%85%A2/">慢</a>
    
    <a href="http://www.kikt.top/tags/%E6%89%93%E5%8C%85/">打包</a>
    
    <a href="http://www.kikt.top/tags/%E6%89%AB%E7%A0%81/">扫码</a>
    
    <a href="http://www.kikt.top/tags/%E6%8F%92%E4%BB%B6/">插件</a>
    
    <a href="http://www.kikt.top/tags/%E6%96%87%E6%A1%A3/">文档</a>
    
    <a href="http://www.kikt.top/tags/%E6%A8%A1%E6%9D%BF/">模板</a>
    
    <a href="http://www.kikt.top/tags/%E6%B8%B2%E6%9F%93/">渲染</a>
    
    <a href="http://www.kikt.top/tags/%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90/">源码解析</a>
    
    <a href="http://www.kikt.top/tags/%E7%81%B0%E5%BA%A6/">灰度</a>
    
    <a href="http://www.kikt.top/tags/%E7%82%B9%E5%87%BB/">点击</a>
    
    <a href="http://www.kikt.top/tags/%E7%84%A6%E7%82%B9/">焦点</a>
    
    <a href="http://www.kikt.top/tags/%E7%88%AC%E8%99%AB/">爬虫</a>
    
    <a href="http://www.kikt.top/tags/%E7%8A%B6%E6%80%81%E7%AE%A1%E7%90%86/">状态管理</a>
    
    <a href="http://www.kikt.top/tags/%E7%99%BE%E5%BA%A6%E5%9C%B0%E5%9B%BE/">百度地图</a>
    
    <a href="http://www.kikt.top/tags/%E7%BC%96%E7%A0%81/">编码</a>
    
    <a href="http://www.kikt.top/tags/%E7%BC%96%E7%A8%8B%E6%8A%80%E5%B7%A7/">编程技巧</a>
    
    <a href="http://www.kikt.top/tags/%E7%BC%96%E8%AF%91/">编译</a>
    
    <a href="http://www.kikt.top/tags/%E7%BF%BB%E8%AF%91/">翻译</a>
    
    <a href="http://www.kikt.top/tags/%E8%93%9D%E7%89%99/">蓝牙</a>
    
    <a href="http://www.kikt.top/tags/%E8%A1%A8%E5%8D%95/">表单</a>
    
    <a href="http://www.kikt.top/tags/%E8%B7%AF%E7%94%B1/">路由</a>
    
    <a href="http://www.kikt.top/tags/%E8%BF%87%E5%9C%BA%E5%8A%A8%E7%94%BB/">过场动画</a>
    
    <a href="http://www.kikt.top/tags/%E9%9A%90%E7%A7%81/">隐私</a>
    
</div>
    </section>

    
<section class="widget">
    <h3 class="widget-title">友情链接</h3>
    <ul class="widget-list">
        
        <li>
            <a target="_blank" href="https://github.com/Caijinglong" title="我的Github">我的Github</a>
        </li>
        
        <li>
            <a target="_blank" href="https://flutter.dev" title="flutter 官网">flutter 官网</a>
        </li>
        
    </ul>
</section>


    <section class="widget">
        <h3 class="widget-title">其它</h3>
        <ul class="widget-list">
            <li><a href="http://www.kikt.top/index.xml">文章 RSS</a></li>
        </ul>
    </section>
</div>
        </div>
    </div>
</div>
<footer id="footer">
    <div class="container">
        <div>
            <p>如果我的blog对您有帮助,而您愿意捐赠 😆, 可以用下面的二维码</p>
            <img style="width:5rem;" src="https://dev.azure.com/cjlspy/844861b8-ee45-48a3-b430-b974684baaaa/_apis/git/repositories/bf4da1c1-1cfa-4f54-8a75-745cce2b737c/items?versionDescriptor%5Bversion%5D=master&versionDescriptor%5BversionOptions%5D=0&versionDescriptor%5BversionType%5D=0&resolveLfs=true&%24format=octetStream&api-version=5.0&path=%2F20190424141834.png"/>
            <img style="width:5rem; padding-left: 2rem;" 
            src="https://dev.azure.com/cjlspy/844861b8-ee45-48a3-b430-b974684baaaa/_apis/git/repositories/bf4da1c1-1cfa-4f54-8a75-745cce2b737c/items?versionDescriptor%5Bversion%5D=master&versionDescriptor%5BversionOptions%5D=0&versionDescriptor%5BversionType%5D=0&resolveLfs=true&%24format=octetStream&api-version=5.0&path=%2F20190424141931.png"/>
            <img style="width:5rem; padding-left: 2rem;" 
            src="https://dev.azure.com/cjlspy/844861b8-ee45-48a3-b430-b974684baaaa/_apis/git/repositories/bf4da1c1-1cfa-4f54-8a75-745cce2b737c/items?versionDescriptor%5Bversion%5D=master&versionDescriptor%5BversionOptions%5D=0&versionDescriptor%5BversionType%5D=0&resolveLfs=true&%24format=octetStream&api-version=5.0&path=%2F20190424142013.png"/>
        </div>
        &copy; 2018 <a href="http://www.kikt.top">caijinglong的博客 By caijinglong</a>.
        Powered by <a rel="nofollow noreferer noopener" href="https://gohugo.io" target="_blank">Hugo</a>.
        <a href="http://www.flysnow.org/" target="_blank">Theme</a> based on <a href="https://github.com/rujews/maupassant-hugo" target="_blank">maupassant</a>.<br/>
        本站所有内容基于<a href="https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh" style="color:coral">CC4.0</a>协议发布,需要转载必须署名
        <br/>
        如果有问题可以<a href="mailto:cjl_spy@163.com">email联系我</a>
        <br/>
        京ICP备18038473号-1
        
    </div>
</footer>


    <script type="text/javascript" src="/js/prism.js" async="true"></script>
    <script type="text/javascript">
    
    (function(){
        $("pre code").parent().addClass("line-numbers")
    }())

    window.MathJax = {
        tex2jax: {
            inlineMath: [ ['$','$'] ],
            processEscapes: true
        }
    };
    </script>
    <script src='https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/MathJax.js?config=TeX-MML-AM_CHTML' async></script>

<a id="rocket" href="#top"></a>
<script type="text/javascript" src="/js/totop.js?v=0.0.0" async=""></script>



<script type="text/javascript" src="//busuanzi.ibruce.info/busuanzi/2.3/busuanzi.pure.mini.js" async></script>




</body>
</html>
